package jflowmap.model.graph;

import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import jflowmap.model.ontology.Context;
import jflowmap.model.ontology.Dimension;
import jflowmap.model.ontology.Filters;
import jflowmap.model.ontology.Match;
import jflowmap.model.ontology.Measurement;
import jflowmap.model.ontology.Value;

import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Ordering;
import com.google.common.collect.Sets;

public final class Graph {
	private final String name;
	private final Context context;
	private final Set<Dimension> valueDimensions = Sets.newHashSet();
	private final Set<Dimension> sourceDimensions = Sets.newHashSet();
	private final Set<Dimension> targetDimensions = Sets.newHashSet();
	
	private final Set<Node> sourceNodes = Sets.newHashSet();
	private final Set<Node> targetNodes = Sets.newHashSet();
	private final Set<Edge> edges = Sets.newHashSet();
	private final Map<Node, Map<Node, Edge>> edgeMap = new HashMap<Node, Map<Node, Edge>>();
	
	private final Set<Measurement> measurements = Sets.newHashSet();
	private final Set<Value> values = Sets.newHashSet();
	
	
	private Dimension valueDimension;
	
	public Collection<Value> getValues() {
		return Lists.newArrayList(values);
	}
	
	public Collection<Measurement> getMeasurements() {
		return Lists.newArrayList(measurements);
	}
	
	public Collection<Value> getValuesMeasuredBy (final Measurement measurement) {
		return Lists.newLinkedList(Iterables.filter(values, new Predicate<Value>() {
			@Override
			public boolean apply(Value value) {
				return measurement.equals(value.getMeasurement());
			}
			
		}));
	}
	
	public Collection<Value> getValuesMatching (final Match match) {
		return Lists.newLinkedList(Iterables.filter(values, new Predicate<Value>() {
			@Override
			public boolean apply(Value value) {
				return match.matches(value);
			}
			
		}));
	}
	
	public Value getMinValueForAll (String sourceDimensionParentName) {
		return getMinValueForAll(context.getDimension(sourceDimensionParentName));
	}
	
	public Value getMaxValueForAll (String sourceDimensionParentName) {
		return getMaxValueForAll(context.getDimension(sourceDimensionParentName));
	}
	
	public Value getValueForAll (String sourceDimensionParentName, Comparator<Value> cmp) {
		return getValueForAll(context.getDimension(sourceDimensionParentName), cmp);
	}
	

	private Map<Dimension, Value> maxValues = Maps.newHashMap();
	private Map<Dimension, Value> minValues = Maps.newHashMap();
	
	public void reload() {
		maxValues.clear();
		minValues.clear();
	}
	
	public Value getMinValueForAll (final Dimension sourceDimensionParent) {
		if (!minValues.containsKey(sourceDimensionParent)) {
			minValues.put(sourceDimensionParent, getValueForAll(sourceDimensionParent, new Comparator<Value>() {
				@Override
				public int compare(Value o1, Value o2) {
					if (o1.getValue() < o2.getValue()) {
						return 1;
					}
					return -1;
				}
			}));
		}
		
		return minValues.get(sourceDimensionParent);
	}
	
	public Value getMaxValueForAll (final Dimension sourceDimensionParent) {
		if (!maxValues.containsKey(sourceDimensionParent)) {
			maxValues.put(sourceDimensionParent, getValueForAll(sourceDimensionParent, new Comparator<Value>() {
				@Override
				public int compare(Value o1, Value o2) {
					if (o1.getValue() > o2.getValue()) {
						return 1;
					}
					return -1;
				}
			}));
		}
		
		return maxValues.get(sourceDimensionParent);
	}
	
	public Value getValueForAll (final Dimension sourceDimensionParent, final Comparator<Value> comparator) {
		final Dimension sourceDimension = sourceDimensionParent;		
		Value result = Context.missingValue;
		
		for (Dimension targetDimension : targetDimensions) {
			for (Dimension valueDimension : valueDimensions) {
				//final Set<Value> values = context.require(sourceDimension, targetDimension, valueDimension);				
				Set<Value> values = context.getValues(
						Filters.matching(new Match(sourceDimension, targetDimension, valueDimension)));
				
				if (values.size() == 1) {
					if (result == Context.missingValue) { 
						result = values.iterator().next();
						
					} else {
						final Value tmp = values.iterator().next();
						if (comparator.compare(tmp, result) == 1) {
							result = tmp;
						}
					}
				}
			}
		}
		
		if (result == Context.missingValue) {
			System.out.println("No aggregate value for " +  sourceDimensionParent + " in "
							   + getCommonTargetDimension() + " and " + getCommonValueDimension() 
							   + " - define an aggregate!");
		}
		
		return result;	
	}
	
	public Set<Node> getTargetNodes() {
		return Collections.unmodifiableSet(targetNodes);
	}
	
	public Set<Node> getSourceNodes() {
		return Collections.unmodifiableSet(sourceNodes);
	}
	
	public Set<Node> nodes() {
		return Sets.union(sourceNodes, targetNodes);
	}
	
	public Dimension getCommonTargetDimension () {
		return getCommonDimension(targetNodes);
	}
	
	public Dimension getCommonSourceDimension () {
		return getCommonDimension(sourceNodes);
	}
	
	private Dimension getCommonDimension (Set<Node> nodes) {
		Set<Dimension> dimensions = null;
		for (Node n: nodes) {
			if (dimensions == null) dimensions = Sets.newHashSet(n.getDimension());
			else dimensions = Sets.union(dimensions, Sets.newHashSet(n.getDimension()));
		}
		
		final Set<Dimension> ancestors = Dimension.getCommonAncestorsOf(dimensions);
		if (ancestors.size() > 1) {
			throw new IllegalStateException("Expected one common dimension in graph, got " 
											+ ancestors.size() + "!");
		}
		return ancestors.iterator().next();
	}
	
	public Graph (final Context context, final String name) {
		this.context = context;
		this.name = name;
	}
	
	public void finish() {
		this.valueDimension = calculateCommonValueDimension();
	}
	
	public Dimension getCommonValueDimension() {
		return this.valueDimension;
	}
	
	private Dimension calculateCommonValueDimension() {
		final Set<Dimension> ancestors = Dimension.getCommonAncestorsOf(valueDimensions);
		if (ancestors.size() != 1) {
			throw new IllegalStateException("Graph with edges containing incompatible dimensions!");
		}
		return ancestors.iterator().next();
	}
	
	public List<Dimension> getValueDimensions() {
		List<Dimension> tmp = Lists.newLinkedList(valueDimensions);
		Collections.sort(tmp, Ordering.usingToString());
		return tmp;
	}

	public void addSourceNode(Dimension source) {
		sourceDimensions.add(source);
		sourceNodes.add(new Node(source, this));		
	}

	public void addTargetNode(Dimension target) {
		targetDimensions.add(target);
		targetNodes.add(new Node(target, this));
	}

	public void addValueDimension(Dimension valueDimension) {
		valueDimensions.add(valueDimension);
	}

	public void createEdges() {
		for (Node source: sourceNodes) {
			final Map<Node, Edge> targetMap = new HashMap<Node, Edge>();
			edgeMap.put(source, targetMap);
			
			for (Node target: targetNodes) {
				final Edge edge = new Edge(source, target, this);				
				for (Dimension valueDimension: valueDimensions) {
					edge.addEdgeValue(valueDimension, Context.missingValue);
				}
				edges.add(edge);
				targetMap.put(target, edge);
			}
			
		}
	}
	
	public void addEdgeValue(Dimension source, Dimension target, Value value) {
		Dimension valueDimension = value.hasAnyOf(valueDimensions);
		edgeMap.get(new Node(source, this)).get(new Node(target, this)).addEdgeValue(valueDimension, value);
		
		if (value != Context.missingValue) {
			values.add(value);
			measurements.add(value.getMeasurement());
		}
	}

	@Override
	public String toString() {
		StringBuilder builder = new StringBuilder();
		for (Map<Node, Edge> targetMap : edgeMap.values()) {
			for (Edge edge: targetMap.values()) {
				builder.append(edge);
				builder.append("\n");
			}
		}
		return builder.toString();
	}

	public String getName() {
		return name;
	}

	public Context getContext() {
		return context;
	}
}
